/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ArkFile, AstTreeUtils, ts} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData, FileMatcher, MatcherCallback, MatcherTypes} from '../../Index';
import {Defects} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {RuleListUtil} from "../../utils/common/DefectsList";
import {IssueReport} from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoThrowLiteralCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: "docs/no-throw-literal-check.md",
    description: "Disallow throwing literals as exceptions",
};

type Options = [{
    allowThrowingAny: boolean,
    allowThrowingUnknown: boolean
}]

export class NoThrowLiteralCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private allowThrowingAny = false;
    private allowThrowingUnknown = false;
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatcherCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        };
        return [fileMatcherCb];
    }

    public check = (arkFile: ArkFile) => {
        if (this.rule && this.rule.option) {
            const option = this.rule.option as Options;
            if (option.length > 0) {
                this.allowThrowingAny = option[0].allowThrowingAny;
                this.allowThrowingUnknown = option[0].allowThrowingUnknown;
            }
        }
        const code = arkFile.getCode();
        if (!code) {
            return;
        }
        const asRoot = AstTreeUtils.getASTNode(arkFile.getName(), code);
        const sourceFileObject = ts.getParseTreeNode(asRoot);
        if (sourceFileObject == undefined) {
            return;
        }

        this.loopNode(arkFile, asRoot, sourceFileObject);
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFile, aNode: ts.Node) {
        const children = aNode.getChildren();
        for (const child of children) {
            if (ts.isThrowStatement(child)) {
                this.processThrowAST(child, sourceFile, targetFile);
            }
            this.loopNode(targetFile, sourceFile, child);
        }
    }

    private processThrowAST(child: ts.ThrowStatement, sourceFile: ts.SourceFile, arkFile: ArkFile): void {
        let positionInfo = this.getPositionInfo(child.expression, sourceFile);
        if (child.expression && ts.isPropertyAccessExpression(child.expression)) {
            let name = child.expression.name.getText();
            let className = '';
            if (ts.isIdentifier(child.expression.expression)) {
                className = child.expression.expression.getText();
            }
            sourceFile.statements.forEach((node) => {
                if (ts.isVariableStatement(node)) {
                    for (const declaration of node.declarationList.declarations) {
                        const nameText = declaration.name.getText();
                        if (nameText === className) {
                            if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
                                for (const property of declaration.initializer.properties) {
                                    if (ts.isPropertyAssignment(property)) {
                                        if (property.name.getText() === name) {
                                            if (this.isCommonKind(property.initializer.kind)) {
                                                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            });
        }
        if (child.expression && ts.isCallExpression(child.expression)) {
            let name = child.expression.expression.getText();
            for (const node of sourceFile.statements) {
                if (ts.isFunctionDeclaration(node)) {
                    if (node.name?.getText() == name) {
                        for (const childNode of node.body!.statements) {
                            if (ts.isReturnStatement(childNode)) {
                                if (this.isCommonKind(childNode.expression?.kind)) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }
        if (child.expression && ts.isIdentifier(child.expression)) {
            let name = child.expression.getText();
            sourceFile.statements.forEach((node) => {
                if (ts.isVariableStatement(node)) {
                    for (let declaration of node.declarationList.declarations) {
                        const nameText = declaration.name.getText();
                        if (nameText == name) {
                            if (this.isCommonKind(declaration.initializer?.kind)) {
                                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                break;
                            }
                            if (declaration.initializer && ts.isAsExpression(declaration.initializer)) {
                                if (declaration.initializer.type.kind === ts.SyntaxKind.AnyKeyword && !this.allowThrowingAny) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                    break;
                                } else if (declaration.initializer.type.kind === ts.SyntaxKind.UnknownKeyword && !this.allowThrowingUnknown) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                    break;
                                }
                            }
                            if (declaration.type) {
                                if (declaration.type!.kind === ts.SyntaxKind.NullKeyword) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                    break;
                                } else if (declaration.type!.kind === ts.SyntaxKind.UnknownKeyword && !this.allowThrowingUnknown) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                    break;
                                }
                            }
                            if (declaration.initializer && ts.isNewExpression(declaration.initializer)) {
                                let name = declaration.initializer.expression.getText();
                                if (!name.includes('Error')) {
                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                }
                                break;
                            }
                        }
                    }
                }
            });
            if (child?.parent?.parent) {
                if (ts.isFunctionDeclaration(child?.parent?.parent)) {
                    child?.parent?.parent?.parameters.forEach((param) => {
                        if (param.name.getText() === name) {
                            if (param.type!.kind === ts.SyntaxKind.AnyKeyword && !this.allowThrowingAny) {
                                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                            } else if (param.type!.kind === ts.SyntaxKind.UnknownKeyword && !this.allowThrowingUnknown) {
                                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                            }
                        }
                    });
                }
            }
            if (name === 'undefined') {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            }
        }
        if (child.expression && ts.isNewExpression(child.expression)) {
            if (child.expression.expression && ts.isIdentifier(child.expression.expression)) {
                let name = child.expression.expression.getText();
                for (const node of sourceFile.statements) {
                    if (ts.isClassDeclaration(node)) {
                        if (node.name?.getText() == name) {
                            if (node.heritageClauses) {
                                for (const heritageClause of node.heritageClauses) {
                                    if (ts.isHeritageClause(heritageClause)) {
                                        heritageClause.types.forEach((type) => {
                                            if (ts.isExpressionWithTypeArguments(type)) {
                                                if (!type.expression.getText().includes('Error')) {
                                                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                                                }
                                            }
                                        });
                                    }
                                }
                            } else {
                                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                            }
                        }
                    }
                }
            }
        }
        if (child.expression) {
            if (child.expression.kind === ts.SyntaxKind.NullKeyword) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (child.expression.kind === ts.SyntaxKind.FalseKeyword) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (child.expression.kind === ts.SyntaxKind.TrueKeyword) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            }
        }
        if (child.expression) {
            if (ts.isTemplateExpression(child.expression)) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (ts.isBinaryExpression(child.expression)) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (ts.isNumericLiteral(child.expression)) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (ts.isStringLiteral(child.expression)) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            } else if (ts.isObjectLiteralExpression(child.expression)) {
                this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
            }
        }
        if (child.expression && ts.isParenthesizedExpression(child.expression)) {
            if (ts.isBinaryExpression(child.expression.expression)) {
                if (ts.isStringLiteral(child.expression.expression.right)) {
                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                }
            }
        }
        if (child.expression && ts.isAsExpression(child.expression)) {
            if (child.expression.type) {
                if (child.expression.type.kind === ts.SyntaxKind.AnyKeyword && !this.allowThrowingAny) {
                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                } else if (child.expression.type.kind === ts.SyntaxKind.UnknownKeyword && !this.allowThrowingUnknown) {
                    this.addIssueReport(arkFile, positionInfo.startPosition.line + 1, positionInfo.startPosition.character + 1, positionInfo.endPosition.character + 1, this.metaData.description);
                }
            }
        }
    }

    private isCommonKind(kind?: ts.SyntaxKind): boolean {
        const excludedKinds = [
            ts.SyntaxKind.StringLiteral,
            ts.SyntaxKind.FalseKeyword,
            ts.SyntaxKind.TrueKeyword,
            ts.SyntaxKind.NumericLiteral,
            ts.SyntaxKind.BigIntLiteral
        ];
        return kind === undefined ? false : (excludedKinds.includes(kind));
    }

    private getPositionInfo(expression: ts.Expression, sourceFile: ts.SourceFileLike) {
        const start = expression.getStart();
        const end = expression.getEnd();
        const startPositionInfo = sourceFile.getLineAndCharacterOfPosition(start);
        const endPositionInfo = sourceFile.getLineAndCharacterOfPosition(end);
        return {
            startPosition: startPositionInfo,
            endPosition: endPositionInfo
        };
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string) {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.defects.push(defect);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
}